package cn.edu.cug.cs.gtl.oi.utils.editors;

import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

import javax.swing.*;

import com.openinventor.inventor.SbColor;
import com.openinventor.inventor.SbVec3f;
import com.openinventor.inventor.SoPreferences;
import com.openinventor.inventor.actions.SoGLRenderAction;
import com.openinventor.inventor.fields.SoMFColor;
import com.openinventor.inventor.nodes.*;
import com.openinventor.inventor.sensors.SoNodeSensor;
import com.openinventor.inventor.viewercomponents.awt.IRenderAreaInteractive;
import com.openinventor.inventor.viewercomponents.awt.tools.SliderPanel;
import com.openinventor.inventor.viewercomponents.nodes.SceneInteractor.CameraMode;

import cn.edu.cug.cs.gtl.oi.utils.ViewerComponentsFactory;

/**
 * This class is used to edit the material properties of an <b>SoMaterial</b>
 * node. The editor can also directly be used using callbacks instead of
 * attaching it to a node. The component consists of a render area displaying a
 * test sphere, some sliders, a set of radio buttons, and a menu. The sphere
 * displays the current material being edited. There is one slider for each
 * material coefficient. Those fields are ambient, diffuse, specular, emissive
 * (all of which are colors) ; and transparency and shininess (which are scalar
 * values). A color editor can be opened to edit the color slider base color. A
 * material list displays palettes of predefined materials from which to choose.
 * <br>
 * <br>
 * The editor can currently be attached to only one material at a time.
 * Attaching two different materials will automatically detach the first one
 * before attaching the second. <br>
 * <br>
 * Note : systems may define the environment variable OIV_MATERIALS_PATH to
 * indicate the path to materials. Otherwise, under the OIVJHOME directory, the
 * data/materials directory may contain materials.
 *
 */
public class MaterialEditor extends JFrame
{

  /**
   * Update frequency: how often new values should be sent to the node or to the
   * editor's listener.
   */
  public enum UpdateMode
  {
    CONTINUOUS, AFTER_ACCEPT
  }

  // menu items
  private static final int k_MATERIAL_LIST = 0;
  private static final int k_CONTINUOUS = 1;
  private static final int k_AFTER_ACCEPT = 2;
  private static final int k_EDIT_CLOSE = 3;

  // ids used in array of sliders and toggles
  // Note: the order (amb/diff/spec/Emiss then the rest) is important
  // for array of sliders.
  private static final int k_AMBIENT_ID = 0;
  private static final int k_DIFFUSE_ID = 1;
  private static final int k_SPECULAR_ID = 2;
  private static final int k_EMISSIVE_ID = 3;
  private static final int k_SHININESS_ID = 4;
  private static final int k_TRANSPARENCY_ID = 5;

  // constants for callback functions (separate bits)
  private static final long k_none = 0x0000;
  private static final long k_ambient = 0x0001;
  private static final long k_diffuse = 0x0002;
  private static final long k_specular = 0x0004;
  private static final long k_emissive = 0x0008;

  private static final String fileSep = System.getProperty("file.separator");

  private static final float[][] tileColors =
      { { .3F, .3F, .3F }, { .6F, .6F, .6F }, { .6F, .6F, .6F }, { .3F, .3F, .3F } };
  private static final float[][] tileCoords = { { -3, 3, 0 }, { 0, 3, 0 }, { 3, 3, 0 }, { -3, 0, 0 }, { 0, 0, 0 },
      { 3, 0, 0 }, { -3, -3, 0 }, { 0, -3, 0 }, { 3, -3, 0 } };

  private SoMaterial material; // material we are editing

  private int index; // material index number
  private SoNodeSensor sensor;
  private java.util.List<Listener> editorListeners;
  private JButton acceptButton;
  private JRadioButton radioButtons[]; // 4
  private JCheckBox checkboxButtons[]; // 4
  private UpdateMode updateFreq;

  private ColorEditor colorEditor;
  private SliderPanel[] sliders; // 6
  private boolean changedIt[]; // 6
  private MaterialList materialList;
  private boolean ignoreCallback; // true while callback should be ignored

  private IRenderAreaInteractive renderArea;
  private SoMaterial localMaterial; // local copy of the material
  private SoDirectionalLight light0;
  private SoDirectionalLight light1;
  private SoBaseColor tileColor;
  private SoSeparator root;

  private long activeColor; // field which color editor edits

  private JMenuBar menubar;
  private JMenu menuEdit;
  private JMenuItem menuMatList, menuClose;
  private JCheckBoxMenuItem menuCheckCont;
  private JCheckBoxMenuItem menuCheckMan;

  public MaterialEditor()
  {
    super("Material Editor");

    // Set up render area
    renderArea = ViewerComponentsFactory.createRenderAreaInteractive();
    renderArea.setCameraType(CameraMode.ORTHOGRAPHIC);
    renderArea.getSceneInteractor().enableHeadlight(false);
    renderArea.setTransparencyType(SoGLRenderAction.TransparencyTypes.NO_SORT);

    addComponentListener(new ListenVisibility());
    activeColor = k_none;
    ignoreCallback = false;
    editorListeners = new ArrayList<MaterialEditor.Listener>();
    sliders = new SliderPanel[6];
    changedIt = new boolean[6];
    for ( int i = 0; i < 6; i++ )
      changedIt[i] = false;

    // create local scene graph for viewing materials
    localMaterial = new SoMaterial();
    light0 = new SoDirectionalLight();
    light1 = new SoDirectionalLight();
    tileColor = new SoBaseColor();
    root = new SoSeparator();
    SoComplexity complexity = new SoComplexity();
    SoSphere sphere = new SoSphere();
    SoCoordinate3 coord = new SoCoordinate3();
    SoQuadMesh quadMesh = new SoQuadMesh();
    SoMaterialBinding matBinding = new SoMaterialBinding();
    SoLightModel lightModel1 = new SoLightModel();
    SoLightModel lightModel2 = new SoLightModel();

    root.addChild(lightModel1);
    root.addChild(tileColor);
    root.addChild(matBinding);
    root.addChild(coord);
    root.addChild(quadMesh);
    root.addChild(lightModel2);
    root.addChild(light0);
    root.addChild(light1);
    root.addChild(localMaterial);
    root.addChild(complexity);
    root.addChild(sphere);
    renderArea.setSceneGraph(root);

    // setup the scene viewing
    SoOrthographicCamera camera = (SoOrthographicCamera) renderArea.getSceneInteractor().getCamera();
    camera.position.setValue(0, 0, 2);
    camera.nearDistance.setValue(1);
    camera.farDistance.setValue(3);
    camera.height.setValue(2);
    complexity.value.setValue(.8F);
    sphere.radius.setValue(.85F);

    // setup the sphere background
    tileColor.rgb.setValues(0, tileColors);
    matBinding.value.setValue(SoMaterialBinding.Bindings.PER_FACE);
    coord.point.setValues(0, tileCoords);
    quadMesh.verticesPerColumn.setValue(3);
    quadMesh.verticesPerRow.setValue(3);
    lightModel1.model.setValue(SoLightModel.Models.BASE_COLOR);
    lightModel2.model.setValue(SoLightModel.Models.PER_VERTEX_PHONG);

    // default for lights
    light0.direction.setValue(.556F, -.623F, -.551F);
    light1.direction.setValue(-.556F, -.623F, -.551F);

    // set up the material sensor -
    // this tells us if someone else changed the material we attach to
    // (the sensor gets attached later)
    sensor = new SoNodeSensor(new SensorTask());

    // build the component tree
    buildGUI();

    // unmap on window manager close button
    WindowListener l = new WindowAdapter()
    {
      @Override
      public void windowClosing(WindowEvent e)
      {
        setVisible(false);
      }
    };
    addWindowListener(l);

    menuCheckCont.setSelected(true);
    menuCheckMan.setSelected(false);
    setUpdateFrequency(UpdateMode.CONTINUOUS);
    pack();
  }

  @Override
  protected void finalize() throws Throwable
  {
    if ( isAttached() )
      detach();
  }

  /**
   * Attach the editor to the given node and edits the first material.
   */
  public void attach(SoMaterial material)
  {
    attach(material, 0);
  }

  /**
   * Attach the editor to the given node and edits the material of the given
   * index.
   */
  public void attach(SoMaterial mtl, int material_index)
  {
    if ( isAttached() )
      detach();

    if ( (mtl != null) && (material_index >= 0) )
    {
      material = mtl;
      index = material_index;

      // set all parameters as unchanged by the editor
      for ( int i = 0; i < 6; i++ )
        changedIt[i] = false;
      if ( isVisible() )
        activate(); // updates and attaches sensor
    }
  }

  /**
   * Detach this editor from the material node.
   */
  public void detach()
  {
    if ( !isAttached() )
      return;

    deactivate(); // detached sensor
    material = null;
  }

  /**
   * Check if this editor is attached to a material node.
   *
   * @return true if this editor is attached to a material node.
   */
  public boolean isAttached()
  {
    return (material != null);
  }

  /**
   * Adds the specified listener to receive editor's events.<br>
   * If listener l is null, no exception is thrown and no action is performed.
   * At the time dictated by {@link #setUpdateFrequency(UpdateMode)}, the
   * listener will be called with the new material.
   *
   * @param listener
   *          the editor listener
   */
  public void addListener(Listener listener)
  {
    if ( listener != null )
      editorListeners.add(listener);
  }

  /**
   * Removes the specified listener if present so that it no longer receives
   * editor's events.
   *
   * @param listener
   *          the editor listener
   * @return true if specified listener has been removed.
   */
  public boolean removeListener(Listener listener)
  {
    return editorListeners.remove(listener);
  }

  private void invokeValueChanged(SoMaterial material)
  {
    for ( Listener listener : editorListeners )
      listener.valueChanged(material);
  }

  /**
   * Set the update frequency of when editor's listeners should be called
   * (default CONTINUOUS).
   */
  public void setUpdateFrequency(UpdateMode freq)
  {
    if ( updateFreq == freq )
      return;

    updateFreq = freq;

    // show/hide the acceptButton
    if ( acceptButton != null )
    {
      acceptButton.setVisible(updateFreq == UpdateMode.AFTER_ACCEPT);
      pack();
    }

    // update the attached node if we switch to continuous
    if ( (material != null) && (updateFreq == UpdateMode.CONTINUOUS) )
    {
      copyMaterial(material, index, localMaterial, 0);
      undoIgnoresIfChanged();
    }
  }

  /**
   * Get the update frequency of when editor's listeners should be called
   * (default CONTINUOUS).
   */
  public UpdateMode getUpdateFrequency()
  {
    return updateFreq;
  }

  @Override
  public void setVisible(boolean b)
  {
    super.setVisible(b);
    if ( (colorEditor != null) && (activeColor != k_none) )
      colorEditor.setVisible(b);
    if ( materialList != null )
      materialList.setVisible(b);
  }

  /**
   * Set new values for the material editor.
   */
  public void setMaterial(SoMaterial mtl)
  {
    // copy the new material locally and update the sliders
    copyMaterial(localMaterial, 0, mtl, 0);
    updateLocalComponents();

    // signal that values have been loaded into all fields
    for ( int i = 0; i < 6; i++ )
      changedIt[i] = true;

    // update the attached material
    if ( (material != null) && (updateFreq == UpdateMode.CONTINUOUS) )
    {
      // detach the sensor while we update the fields
      sensor.detach();
      copyMaterial(material, index, mtl, 0);
      undoIgnoresIfChanged();
      sensor.attach(material);
    }

    // invoke the callbacks with the new material
    if ( updateFreq == UpdateMode.CONTINUOUS )
      invokeValueChanged(localMaterial);
  }

  /**
   * Get current material.
   */
  public SoMaterial getMaterial()
  {
    return localMaterial;
  }

  private JMenuBar buildMenu()
  {
    JPopupMenu.setDefaultLightWeightPopupEnabled(false);

    menubar = new JMenuBar();

    menuEdit = new JMenu("Edit");
    menubar.add(menuEdit);
    menuEdit.addActionListener(new MenuDisplay());

    menuMatList = new JMenuItem("Material List");

    ButtonGroup groupEdit = new ButtonGroup();
    menuCheckCont = new JCheckBoxMenuItem("Continuous");
    menuCheckMan = new JCheckBoxMenuItem("Manual");
    groupEdit.add(menuCheckCont);
    groupEdit.add(menuCheckMan);

    menuClose = new JMenuItem("Close");

    menuEdit.add(menuMatList);
    menuEdit.addSeparator();
    menuEdit.add(menuCheckCont);
    menuEdit.add(menuCheckMan);
    menuEdit.addSeparator();
    menuEdit.add(menuClose);

    menuMatList.addActionListener(new MenuPick(k_MATERIAL_LIST));
    menuCheckCont.addItemListener(new MenuPick(k_CONTINUOUS));
    menuCheckMan.addItemListener(new MenuPick(k_AFTER_ACCEPT));
    menuClose.addActionListener(new MenuPick(k_EDIT_CLOSE));
    return menubar;
  }

  private JPanel buildSliders()
  {
    JPanel slidersPanel = new JPanel(new GridBagLayout());

    int i;

    for ( i = 0; i < 6; i++ )
    {
      sliders[i] = new SliderPanel(0.f, 1.f, 0.f, 2);
      changedIt[i] = false;
    }

    sliders[k_SHININESS_ID].setSliderBackground(Color.WHITE);
    sliders[k_TRANSPARENCY_ID].setSliderBackground(Color.WHITE);

    // Callbacks
    sliders[k_AMBIENT_ID].addSliderPanelListener(new AmbientSlider());
    sliders[k_DIFFUSE_ID].addSliderPanelListener(new DiffuseSlider());
    sliders[k_SPECULAR_ID].addSliderPanelListener(new SpecularSlider());
    sliders[k_EMISSIVE_ID].addSliderPanelListener(new EmissiveSlider());
    sliders[k_SHININESS_ID].addSliderPanelListener(new ShininessSlider());
    sliders[k_TRANSPARENCY_ID].addSliderPanelListener(new TransparencySlider());

    sliders[k_AMBIENT_ID].addInfoText("Amb");
    sliders[k_DIFFUSE_ID].addInfoText("Diff");
    sliders[k_SPECULAR_ID].addInfoText("Spec");
    sliders[k_EMISSIVE_ID].addInfoText("Emis");
    sliders[k_SHININESS_ID].addInfoText("Shininess");
    sliders[k_TRANSPARENCY_ID].addInfoText("Transp");
    for ( i = 0; i < 6; i++ )
      sliders[i].setInfoTextSize(new Dimension(60, 30));

    GridBagConstraints gbc = new GridBagConstraints();
    gbc.fill = GridBagConstraints.BOTH;
    gbc.insets = new Insets(0, 0, 5, 5);

    ButtonGroup diamondGroup = new ButtonGroup();
    radioButtons = new JRadioButton[4];
    checkboxButtons = new JCheckBox[4];
    for ( i = 0; i < 4; i++ )
    {
      gbc.gridx = 0;
      gbc.gridy = i;

      radioButtons[i] = new JRadioButton("", false);
      radioButtons[i].addItemListener(new DiamondButtonPick(i));
      diamondGroup.add(radioButtons[i]);
      slidersPanel.add(radioButtons[i], gbc);

      checkboxButtons[i] = new JCheckBox("", false);
      checkboxButtons[i].addItemListener(new RadioButtonPick(i));
      gbc.gridx += 1;
      slidersPanel.add(checkboxButtons[i], gbc);

      gbc.gridx += 1;
      slidersPanel.add(sliders[i], gbc);
    }
    gbc.gridx = 0;
    gbc.gridy = 4;
    slidersPanel.add(new JLabel(), gbc);
    gbc.gridx += 1;
    slidersPanel.add(new JLabel(), gbc);
    gbc.gridx += 1;
    slidersPanel.add(sliders[4], gbc);

    gbc.gridx = 0;
    gbc.gridy = 5;
    slidersPanel.add(new JLabel(), gbc);
    gbc.gridx += 1;
    slidersPanel.add(new JLabel(), gbc);
    gbc.gridx += 1;
    slidersPanel.add(sliders[5], gbc);

    return slidersPanel;
  }

  /**
   * Builds and layout the edit label with the slider form
   */
  private JPanel buildControls()
  {
    JPanel controlsPanel = new JPanel(new BorderLayout());

    JLabel sliderTitle = new JLabel("Edit Color", Label.LEFT);
    controlsPanel.add(sliderTitle, BorderLayout.NORTH);

    JPanel slidersPanel = buildSliders();
    controlsPanel.add(slidersPanel, BorderLayout.CENTER);

    return controlsPanel;
  }

  /**
   * Builds the GUI and sets up the listeners.
   */
  private void buildGUI()
  {
    JPanel contentPane = new JPanel(new BorderLayout());
    setContentPane(contentPane);

    JMenuBar menuBar = buildMenu();
    setJMenuBar(menuBar);

    final Component canvas = renderArea.getComponent();
    canvas.setPreferredSize(new Dimension(200, 200));
    contentPane.add(canvas, BorderLayout.CENTER);

    JPanel controlsPanel = buildControls();
    contentPane.add(controlsPanel, BorderLayout.EAST);

    JPanel buttonPanel = new JPanel();
    buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
    acceptButton = new JButton("Accept");
    acceptButton.addActionListener(new AcceptButton());
    buttonPanel.add(acceptButton);
    contentPane.add(buttonPanel, BorderLayout.SOUTH);

    if ( updateFreq == UpdateMode.AFTER_ACCEPT )
      acceptButton.setVisible(false);

    // update the sliders based on local scene
    updateLocalComponents();
    pack();
  }

  /**
   * update the sliders/colorEditor based on the local material
   */
  private void updateLocalComponents()
  {
    // update the color sliders to reflect these values
    updateColorSlider(k_AMBIENT_ID, localMaterial.ambientColor.getValueAt(0).getValue());
    updateColorSlider(k_DIFFUSE_ID, localMaterial.diffuseColor.getValueAt(0).getValue());
    updateColorSlider(k_SPECULAR_ID, localMaterial.specularColor.getValueAt(0).getValue());
    updateColorSlider(k_EMISSIVE_ID, localMaterial.emissiveColor.getValueAt(0).getValue());

    // update non color sliders
    ignoreCallback = true;
    sliders[k_SHININESS_ID].setSliderValue(localMaterial.shininess.getValueAt(0));
    sliders[k_TRANSPARENCY_ID].setSliderValue(localMaterial.transparency.getValueAt(0));
    ignoreCallback = false;
    updateColorEditor();
  }

  /**
   * update the colorEditor based on the activeColor flag
   */
  private void updateColorEditor()
  {
    updateColorEditor(false);
  }

  private void updateColorEditor(boolean updateTitle)
  {
    // show/hide the colorEditor
    if ( activeColor == k_none )
    {
      if ( (colorEditor == null) || (!colorEditor.isVisible()) )
        return;
      colorEditor.setVisible(false);
      return;
    }
    else
    {
      if ( colorEditor == null )
      {
        colorEditor = new ColorEditor();
        colorEditor.setCurrentSliders(ColorEditor.NONE);
        colorEditor.addListener(new ColorEditorListener());
        colorEditor.addWindowListener(new ListenColorEditorVisibility());
      }
      colorEditor.setVisible(true);
    }

    // sets the right color, ignoring the colorEditor callback
    ignoreCallback = true;

    switch ( (int) activeColor )
    {
    case (int) k_none :
      break;
    case (int) k_ambient :
      colorEditor.setColor(new SbColor(sliders[k_AMBIENT_ID].getSliderBackground()));
      if ( updateTitle )
        colorEditor.setTitle("Amb");
      break;
    case (int) k_diffuse :
      colorEditor.setColor(new SbColor(sliders[k_DIFFUSE_ID].getSliderBackground()));
      if ( updateTitle )
        colorEditor.setTitle("Diff");
      break;
    case (int) k_specular :
      colorEditor.setColor(new SbColor(sliders[k_SPECULAR_ID].getSliderBackground()));
      if ( updateTitle )
        colorEditor.setTitle("Spec");
      break;
    case (int) k_emissive :
      colorEditor.setColor(new SbColor(sliders[k_EMISSIVE_ID].getSliderBackground()));
      if ( updateTitle )
        colorEditor.setTitle("Emis");
      break;
    default:
      // this is not really perfect because the colorEditor
      // doesn't yet support a multiple attach state (where sliders
      // also reflect the conflicting colors)
      if ( updateTitle )
      {
        StringBuffer title = new StringBuffer(512);
        title.append("Material Color");
        if ( (activeColor & k_ambient) != 0 )
          title.append("Amb");
        if ( (activeColor & k_diffuse) != 0 )
          title.append("Diff");
        if ( (activeColor & k_specular) != 0 )
          title.append("Spec");
        if ( (activeColor & k_emissive) != 0 )
          title.append("Emis");

        title.append(' ');
        title.append("");
        colorEditor.setTitle(title.toString());
      }
      break;
    }
    ignoreCallback = false;
  }

  /**
   * update a color slider (amb/diff/spec/emiss) based of a material color
   * (split the base color from the intensity).
   */
  private void updateColorSlider(int sliderId, float rgb[])
  {
    float max;
    float[] baseColor = new float[3];

    // get color intensity
    max = (rgb[0] > rgb[1]) ? ((rgb[0] > rgb[2]) ? rgb[0] : rgb[2]) : ((rgb[1] > rgb[2]) ? rgb[1] : rgb[2]);

    if ( max == 0 )
      baseColor[0] = baseColor[1] = baseColor[2] = 1;
    else
    {
      // scale the color to full intensity
      float scale = 1 / max;
      baseColor[0] = rgb[0] * scale;
      baseColor[1] = rgb[1] * scale;
      baseColor[2] = rgb[2] * scale;
    }

    // now set the slider to the right value
    ignoreCallback = true;
    sliders[sliderId].setSliderBackground(new Color(baseColor[0], baseColor[1], baseColor[2]));
    sliders[sliderId].setSliderValue(max);
    ignoreCallback = false;
  }

  /**
   * Update a material field when a color slider changes.
   */
  private void updateMaterialColor(SoMFColor editMtlColor, SoMFColor localMtlColor, Color rgb, float intensity)
  {
    SbColor intensityColor = new SbColor(rgb);
    intensityColor.multiply(intensity);

    if ( (editMtlColor != null) && (updateFreq == UpdateMode.CONTINUOUS) )
    {
      // detach the sensor while we update the field
      sensor.detach();
      editMtlColor.set1Value(index, intensityColor);
      if ( editMtlColor.isIgnored() )
        editMtlColor.setIgnored(false);
      sensor.attach(material);
    }
    localMtlColor.setValue(intensityColor);

    // invoke the callbacks with the new material
    if ( updateFreq == UpdateMode.CONTINUOUS )
      invokeValueChanged(localMaterial);
  }

  /**
   * Copies material1 onto material2.
   */
  private void copyMaterial(SoMaterial mat1, int ind1, SoMaterial mat2, int ind2)
  {
    mat1.ambientColor.set1Value(ind1, mat2.ambientColor.getValueAt(ind2));
    mat1.diffuseColor.set1Value(ind1, mat2.diffuseColor.getValueAt(ind2));
    mat1.specularColor.set1Value(ind1, mat2.specularColor.getValueAt(ind2));
    mat1.emissiveColor.set1Value(ind1, mat2.emissiveColor.getValueAt(ind2));
    mat1.shininess.set1Value(ind1, mat2.shininess.getValueAt(ind2));
    mat1.transparency.set1Value(ind1, mat2.transparency.getValueAt(ind2));
  }

  /**
   * For each of the 6 sliders (or sets of sliders) sets the ignore flag of the
   * material node being edited to false if it has been changed.
   */
  private void undoIgnoresIfChanged()
  {
    SoMaterial tmpMtl = material;
    if ( changedIt[k_AMBIENT_ID] )
      tmpMtl.ambientColor.setIgnored(false);
    if ( changedIt[k_DIFFUSE_ID] )
      tmpMtl.diffuseColor.setIgnored(false);
    if ( changedIt[k_SPECULAR_ID] )
      tmpMtl.specularColor.setIgnored(false);
    if ( changedIt[k_EMISSIVE_ID] )
      tmpMtl.emissiveColor.setIgnored(false);
    if ( changedIt[k_SHININESS_ID] )
      tmpMtl.shininess.setIgnored(false);
    if ( changedIt[k_TRANSPARENCY_ID] )
      tmpMtl.transparency.setIgnored(false);
  }

  /**
   * connects the sensor
   */
  private void activate()
  {
    // attach sensor to its material
    if ( isAttached() && (sensor.getAttachedNode() == null) )
    {
      copyMaterial(localMaterial, 0, material, index);
      updateLocalComponents();
      sensor.attach(material); // attach AFTER update
    }
  }

  /**
   * disconnects the sensor
   */
  private void deactivate()
  {
    sensor.detach();
  }

  /**
   * Defines an object which listens for material editor's events.<br>
   * This object will be notified when material has changed.
   *
   */
  public static class Listener
  {
    /**
     * Invoked when material has changed.
     *
     * @param material
     *          the new material
     */
    public void valueChanged(SoMaterial material)
    {};
  }

  class MaterialListListener extends MaterialList.Listener
  {
    @Override
    public void valueChanged(SoMaterial material)
    {
      setMaterial(material);
    }
  }

  class ShininessSlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      if ( (material != null) && (updateFreq == UpdateMode.CONTINUOUS) )
      {
        // detach the sensor while we update the field
        sensor.detach();
        material.shininess.set1Value(index, value);
        if ( material.shininess.isIgnored() )
          material.shininess.setIgnored(false);
        sensor.attach(material);
      }
      localMaterial.shininess.setValue(value);
      changedIt[k_SHININESS_ID] = true;

      // invoke the callbacks with the new material
      if ( updateFreq == UpdateMode.CONTINUOUS )
        invokeValueChanged(localMaterial);
    }
  }

  class TransparencySlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      if ( (material != null) && (updateFreq == UpdateMode.CONTINUOUS) )
      {
        // detach the sensor while we update the field
        sensor.detach();
        material.transparency.set1Value(index, value);
        if ( material.transparency.isIgnored() )
          material.transparency.setIgnored(false);
        sensor.attach(material);
      }
      localMaterial.transparency.setValue(value);
      changedIt[k_TRANSPARENCY_ID] = true;

      // invoke the callbacks with the new material
      if ( updateFreq == UpdateMode.CONTINUOUS )
        invokeValueChanged(localMaterial);
    }
  }

  class AmbientSlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      updateMaterialColor((material != null) ? material.ambientColor : null, localMaterial.ambientColor,
          sliders[k_AMBIENT_ID].getSliderBackground(), value);
      changedIt[k_AMBIENT_ID] = true;
    }
  }

  class DiffuseSlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      updateMaterialColor((material != null) ? material.diffuseColor : null, localMaterial.diffuseColor,
          sliders[k_DIFFUSE_ID].getSliderBackground(), value);
      changedIt[k_DIFFUSE_ID] = true;
    }
  }

  class SpecularSlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      updateMaterialColor((material != null) ? material.specularColor : null, localMaterial.specularColor,
          sliders[k_SPECULAR_ID].getSliderBackground(), value);
      changedIt[k_SPECULAR_ID] = true;
    }
  }

  class EmissiveSlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      updateMaterialColor((material != null) ? material.emissiveColor : null, localMaterial.emissiveColor,
          sliders[k_EMISSIVE_ID].getSliderBackground(), value);
      changedIt[k_EMISSIVE_ID] = true;
    }
  }

  class ListenColorEditorVisibility extends WindowAdapter
  {
    @Override
    public void windowClosed(WindowEvent e)
    {
      for ( int i = 0; i < 4; i++ )
      {
        radioButtons[i].setSelected(false);
        checkboxButtons[i].setSelected(false);
      }
      setVisible(false);
      activeColor = k_none;
    }
  }

  class ListenMaterialListVisibility extends WindowAdapter
  {
    @Override
    public void windowClosed(WindowEvent e)
    {
      setVisible(false);
    }
  }

  class ColorEditorListener extends ColorEditor.Listener
  {
    @Override
    public void valueChanged(SbColor rgbColor)
    {
      Color awtColor = rgbColor.toAWTColor();
      SbColor finalColor = new SbColor();
      boolean updateMaterial = ((material != null) && (updateFreq == UpdateMode.CONTINUOUS));

      if ( ignoreCallback )
        return;

      // check which slider(s) needs to be updated
      //
      // update the slider base color, the local material
      // and the attached material. The final material color is
      // finalColor = baseColor * sliderScaleValue.

      // detach sensor while we update the fields and prevent slider callbacks
      if ( updateMaterial )
        sensor.detach();
      ignoreCallback = true;

      if ( (activeColor & k_ambient) != 0 )
      {
        sliders[k_AMBIENT_ID].setSliderBackground(awtColor);
        finalColor.setValue(new SbVec3f(rgbColor.getValue()));
        finalColor.multiply(sliders[k_AMBIENT_ID].getSliderValue());
        localMaterial.ambientColor.setValue(finalColor);
        changedIt[k_AMBIENT_ID] = true;
      }

      if ( (activeColor & k_diffuse) != 0 )
      {
        // Always update diffuse slider's base color.
        sliders[k_DIFFUSE_ID].setSliderBackground(awtColor);
        finalColor.setValue(new SbVec3f(rgbColor.getValue()));
        finalColor.multiply(sliders[k_DIFFUSE_ID].getSliderValue());
        localMaterial.diffuseColor.setValue(finalColor);
        changedIt[k_DIFFUSE_ID] = true;
        if ( updateMaterial )
        {
          material.diffuseColor.set1Value(index, finalColor);
          if ( material.diffuseColor.isIgnored() )
            material.diffuseColor.setIgnored(false);
        }
      }

      if ( (activeColor & k_specular) != 0 )
      {
        sliders[k_SPECULAR_ID].setSliderBackground(awtColor);
        finalColor.setValue(new SbVec3f(rgbColor.getValue()));
        finalColor.multiply(sliders[k_SPECULAR_ID].getSliderValue());
        localMaterial.specularColor.setValue(finalColor);
        changedIt[k_SPECULAR_ID] = true;
        if ( updateMaterial )
        {
          material.specularColor.set1Value(index, finalColor);
          if ( material.specularColor.isIgnored() )
            material.specularColor.setIgnored(false);
        }
      }

      if ( (activeColor & k_emissive) != 0 )
      {
        sliders[k_EMISSIVE_ID].setSliderBackground(awtColor);
        finalColor.setValue(new SbVec3f(rgbColor.getValue()));
        finalColor.multiply(sliders[k_EMISSIVE_ID].getSliderValue());
        localMaterial.emissiveColor.setValue(finalColor);
        changedIt[k_EMISSIVE_ID] = true;
        if ( updateMaterial )
        {
          material.emissiveColor.set1Value(index, finalColor);
          if ( material.emissiveColor.isIgnored() )
            material.emissiveColor.setIgnored(false);
        }
      }

      // invoke the callbacks with the new material
      if ( updateFreq == UpdateMode.CONTINUOUS )
        invokeValueChanged(localMaterial);

      // re-attach the sensor and callbacks
      if ( updateMaterial )
        sensor.attach(material);
      ignoreCallback = false;
    }
  }

  class SensorTask implements Runnable
  {
    @Override
    public void run()
    {
      if ( isVisible() )
        return;

      // copy edited material over and update sliders/colorEditor
      copyMaterial(localMaterial, 0, material, index);
      updateLocalComponents();
    }
  }

  class ListenVisibility extends ComponentAdapter
  {
    @Override
    public void componentShown(ComponentEvent e)
    {
      activate();
    }

    @Override
    public void componentHidden(ComponentEvent e)
    {
      deactivate();
    }
  }

  class MenuDisplay implements ActionListener
  {
    @Override
    public void actionPerformed(ActionEvent e)
    {
      boolean state = (updateFreq == UpdateMode.CONTINUOUS);
      menuCheckCont.setSelected(state);
      menuCheckMan.setSelected(state);
    }
  }

  class MenuPick implements ActionListener, ItemListener
  {
    int id;

    public MenuPick(int menuId)
    {
      id = menuId;
    }

    @Override
    public void itemStateChanged(ItemEvent e)
    {
      switch ( id )
      {
      case k_CONTINUOUS :
        setUpdateFrequency(UpdateMode.CONTINUOUS);
        doLayout();
        break;
      case k_AFTER_ACCEPT :
        setUpdateFrequency(UpdateMode.AFTER_ACCEPT);
        doLayout();
        break;
      }
    }

    @Override
    public void actionPerformed(ActionEvent e)
    {
      switch ( id )
      {
      case k_MATERIAL_LIST :
        if ( materialList == null )
        {
          String path;
          String tmp = SoPreferences.getValue("OIV_MATERIALS_PATH");
          if ( tmp == null )
          {
            tmp = SoPreferences.getValue("OIVJHOME");

            StringBuffer pathBuffer = new StringBuffer();
            pathBuffer.append(tmp);
            pathBuffer.append(fileSep);
            pathBuffer.append("data");
            pathBuffer.append(fileSep);
            pathBuffer.append("materials");
            path = pathBuffer.toString();
          }
          else
            path = tmp;
          materialList = new MaterialList(path);
          materialList.addListener(new MaterialListListener());
          materialList.addWindowListener(new ListenMaterialListVisibility());
        }
        materialList.setVisible(true);
        break;
      case k_EDIT_CLOSE :
        setVisible(false);
        break;
      default:
        break;
      }
    }
  }

  class RadioButtonPick implements ItemListener
  {
    int id;

    public RadioButtonPick(int menuId)
    {
      id = menuId;
    }

    @Override
    public void itemStateChanged(ItemEvent e)
    {
      int i;

      if ( checkboxButtons[id].isSelected() )
      {
        if ( activeColor == k_none )
          radioButtons[id].setSelected(true);
        else
          for ( i = 0; i < 4; i++ )
            radioButtons[i].setSelected(false);

        switch ( id )
        {
        case k_AMBIENT_ID :
          activeColor |= k_ambient;
          break;
        case k_DIFFUSE_ID :
          activeColor |= k_diffuse;
          break;
        case k_SPECULAR_ID :
          activeColor |= k_specular;
          break;
        case k_EMISSIVE_ID :
          activeColor |= k_emissive;
          break;
        }
      }
      else
      {
        radioButtons[id].setSelected(false);
        // clear the right bit
        switch ( id )
        {
        case k_AMBIENT_ID :
          activeColor &= ~k_ambient;
          break;
        case k_DIFFUSE_ID :
          activeColor &= ~k_diffuse;
          break;
        case k_SPECULAR_ID :
          activeColor &= ~k_specular;
          break;
        case k_EMISSIVE_ID :
          activeColor &= ~k_emissive;
          break;
        }

        // check if a diamond button can be set (i.e. only one slider selected)
        switch ( (int) activeColor )
        {
        case (int) k_ambient :
          radioButtons[k_AMBIENT_ID].setSelected(true);
          break;
        case (int) k_diffuse :
          radioButtons[k_DIFFUSE_ID].setSelected(true);
          break;
        case (int) k_specular :
          radioButtons[k_SPECULAR_ID].setSelected(true);
          break;
        case (int) k_emissive :
          radioButtons[k_EMISSIVE_ID].setSelected(true);
          break;
        }
      }
      updateColorEditor(true);
    }
  }

  class DiamondButtonPick implements ItemListener
  {
    int id;

    public DiamondButtonPick(int menuId)
    {
      id = menuId;
    }

    @Override
    public void itemStateChanged(ItemEvent e)
    {
      int i;
      if ( radioButtons[id].isSelected() )
      {
        checkboxButtons[id].setSelected(true);
        for ( i = 0; i < 4; i++ )
          checkboxButtons[i].setSelected(false);

        switch ( id )
        {
        case k_AMBIENT_ID :
          activeColor = k_ambient;
          break;
        case k_DIFFUSE_ID :
          activeColor = k_diffuse;
          break;
        case k_SPECULAR_ID :
          activeColor = k_specular;
          break;
        case k_EMISSIVE_ID :
          activeColor = k_emissive;
          break;
        }
      }
      else
      {
        checkboxButtons[id].setSelected(false);
        activeColor = k_none;
      }
      updateColorEditor(true);
    }
  }

  class AcceptButton implements ActionListener
  {
    @Override
    public void actionPerformed(ActionEvent e)
    {
      // copy the local material to the edited material
      if ( material != null )
      {
        // detach the sensor while we update the fields
        sensor.detach();
        copyMaterial(material, index, localMaterial, 0);
        undoIgnoresIfChanged();
        sensor.attach(material);
      }

      // Invoke the callbacks with the new material
      invokeValueChanged(localMaterial);
    }
  }
}
